#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
# This source file is part of the Cangjie project, licensed under Apache-2.0
# with Runtime Library Exception.
#
# See https://cangjie-lang.cn/pages/LICENSE for license information.

"""CangjieMigrator build entry"""
import argparse
import json
import logging
import os
import platform
import shutil
import subprocess
import sys
import stat
import tarfile
import zipfile
import re
from enum import Enum
from logging.handlers import TimedRotatingFileHandler
from subprocess import PIPE
from xml.dom import NotSupportedErr


# Optional requests support
try:
    import requests
    HAS_REQUESTS = True
except Exception:
    requests = None
    HAS_REQUESTS = False


# Optional tqdm support
try:
    from tqdm import tqdm as _tqdm
    HAS_TQDM = True

    def tqdm_wrap(*args, **kwargs):
        return _tqdm(*args, **kwargs)
except Exception:
    HAS_TQDM = False

    class DummyTqdm:
        def __init__(self, *args, **kwargs): pass
        def __enter__(self): return self
        def __exit__(self, exc_type, exc, tb): return False
        def update(self, n=1): pass

    def tqdm_wrap(*args, **kwargs):
        # keep signature compatibility but do nothing
        return DummyTqdm()


CANGJIE_REPO = ""

VERSION_LIST_FILE = "version_list.json"

BuildTypeReverse = {
    "Debug": "debug",
    "Release": "release",
    "RelWithDebInfo": "relwithdebinfo"
}


HOME_DIR = os.path.dirname(os.path.abspath(__file__))
BUILD_DIR = os.path.join(HOME_DIR, "build")
UNITTEST_DIR = os.path.join(HOME_DIR, 'unittest', 'Format')
VERSIONS_DIR = os.path.join(HOME_DIR, "versions")
OUTPUT_DIR = os.path.join(HOME_DIR, 'output')
CJM_BUILD_DIR = os.path.join(BUILD_DIR, 'build')
CMAKE_OUTPUT_DIR = os.path.join(HOME_DIR, 'output')
OUTPUT_BIN_DIR = os.path.join(CMAKE_OUTPUT_DIR, 'bin')
LOG_DIR = os.path.join(BUILD_DIR, 'logs')
LOG_FILE = os.path.join(LOG_DIR, 'build.log')


class Platform(Enum):
    WINDOWS = 'windows'
    LINUX = 'linux'
    DARWIN = 'darwin'

    def __str__(self):
        return self.name

    def __repr__(self):
        return str(self)

    def is_windows(self):
        return self == Platform.WINDOWS

    def is_linux(self):
        return self == Platform.LINUX

    def is_darwin(self):
        return self == Platform.DARWIN

    def is_unix(self):
        return self.is_linux() or self.is_darwin()

    @staticmethod
    def argparse(s: str):
        s = s.lower()
        if s == 'windows':
            return Platform.WINDOWS
        elif s == 'linux':
            return Platform.LINUX
        elif s == 'darwin':
            return Platform.DARWIN
        else:
            raise NotSupportedErr(s)

    @staticmethod
    def current():
        return Platform.argparse(platform.system())

class Architecture(Enum):
    X86_64='x86_64'
    ARM64='aarch64'

    def __str__(self):
        return self.name

    def __repr__(self):
        return str(self)

    @staticmethod
    def argparse(s: str):
        if s == Architecture.X86_64.value:
            return Architecture.X86_64
        elif s == Architecture.ARM64.value:
            return Architecture.ARM64
        else:
            raise NotSupportedErr(s)

    @staticmethod
    def current():
        if platform.uname().processor in ["aarch64", "arm", "arm64"]:
            return Architecture.ARM64
        else:
            return Architecture.X86_64


def get_current_build_type():
    return Platform.current().value + '-' + Architecture.current().value


def read_versions():
    version_file = os.path.join(VERSIONS_DIR, VERSION_LIST_FILE)
    with open(version_file, "r", encoding="utf-8") as file:
        data = json.load(file)

    return data


version_list_all = read_versions()
version_list = version_list_all['common']
version_list_deveco = version_list_all['deveco']


def log_output(output):
    """log command output"""
    while True:
        line = output.stdout.readline()
        if not line:
            output.communicate()
            returncode = output.returncode
            if returncode != 0:
                LOG.error('build error: %d!\n', returncode)
                sys.exit(1)
            break
        try:
            LOG.info(line.decode('ascii', 'ignore').rstrip())
        except UnicodeEncodeError:
            LOG.info(line.decode('utf-8', 'ignore').rstrip())


def init_log(name):
    """init log config"""
    if not os.path.exists(LOG_DIR):
        os.makedirs(LOG_DIR)

    log = logging.getLogger(name)
    log.setLevel(logging.DEBUG)
    formatter = logging.Formatter(
        '[%(asctime)s:%(module)s:%(lineno)s:%(levelname)s] %(message)s')
    streamhandler = logging.StreamHandler(sys.stdout)
    streamhandler.setLevel(logging.DEBUG)
    streamhandler.setFormatter(formatter)
    log.addHandler(streamhandler)
    filehandler = TimedRotatingFileHandler(LOG_FILE,
                                           when='W6',
                                           interval=1,
                                           backupCount=60)
    filehandler.setLevel(logging.DEBUG)
    filehandler.setFormatter(formatter)
    log.addHandler(filehandler)
    return log


def generate_cmake_defs(args):
    """convert args to cmake defs"""

    def bool_to_opt(value):
        return 'ON' if value else 'OFF'

    return [
               '-DCMAKE_TOOLCHAIN_FILE=' + HOME_DIR + '/cmake/toolchain/' + args.target_type + '.cmake',
           ]


def redo_with_write(redo_func, path, err):
    # Is the error an access error?
    if not os.access(path, os.W_OK):
        os.chmod(path, stat.S_IWUSR)
        redo_func(path)
    else:
        raise


def mkdir_if_not_exists(dir):
    if not os.path.exists(dir):
        os.mkdir(dir)


def generate_version_list(path, versions):
    if os.path.exists(path):
        os.remove(path)
    with open(path, 'w') as file:
        for version in versions.keys():
            file.write(f"{version}\n")


def download_requests(url, save_path, timeout=30):
    file_name = os.path.basename(url)
    full_path = os.path.join(save_path, file_name)
    os.makedirs(save_path, exist_ok=True)
    if not HAS_REQUESTS:
        raise RuntimeError("requests not available")
    response = requests.get(url, stream=True, verify=False, timeout=timeout)
    # content-length may be missing; avoid int(None)
    header_len = response.headers.get("content-length")
    total_size = int(header_len) if (header_len is not None and header_len.isdigit()) else 0
    block_size = 1024
    _, file_name = os.path.split(url)
    with open(full_path, "wb") as file:
        print(f"Begin to download: {file_name}")
        # use tqdm when available; otherwise the dummy does nothing
        total_arg = (total_size if total_size > 0 else None)
        with tqdm_wrap(total=total_arg, unit="B", unit_scale=True) as progress_bar:
            for data in response.iter_content(block_size):
                progress_bar.update(len(data))
                file.write(data)
        print(f"Complete download at: {full_path}")


def download_curl(url, save_path):
    file_name = os.path.basename(url)
    full_path = os.path.join(save_path, file_name)
    os.makedirs(save_path, exist_ok=True)

    print(f"Begin to download: {file_name} (using curl)")
    subprocess.run([
        "curl", "-L", "-o", full_path, url
    ], check=True)
    print(f"Complete download at: {full_path} (via curl)")


def download_wget(url, save_path):
    file_name = os.path.basename(url)
    full_path = os.path.join(save_path, file_name)
    os.makedirs(save_path, exist_ok=True)

    print(f"Begin to download: {file_name} (using wget)")
    subprocess.run([
        "wget", "-O", full_path, url
    ], check=True)
    print(f"Complete download at: {full_path} (via wget)")


def download(url, save_path):
    if HAS_REQUESTS:
        try:
            download_requests(url, save_path)
            return
        except Exception as e:
            print(f"[requests] download failed: {e}")
            print("Attempting fallback download...")
    else:
        print("[requests] not available; skipping requests path.")

    if shutil.which("curl"):
        try:
            download_curl(url, save_path)
            return
        except Exception as ce:
            print(f"[curl] fallback failed: {ce}")

    if shutil.which("wget"):
        try:
            download_wget(url, save_path)
            return
        except Exception as we:
            print(f"[wget] fallback failed: {we}")

    print("All download methods failed.")
    raise RuntimeError("Download failed via all methods.")


def extract(platform, file_directory, file_name):
    file_path = str(os.path.join(file_directory, file_name))
    if platform.is_windows():
        if not (file_path.endswith(".zip") or os.path.splitext(file_path)[-1] == ".zip"):
            raise ValueError(f"File {file_path} is not .zip on Windows.")
        with zipfile.ZipFile(file_path, 'r') as zip_ref:
            file_list = zip_ref.namelist()
            print(f"Begin to extract file: {file_name} to {file_path}")
            with tqdm_wrap(total=len(file_list), unit='File', miniters=1) as progress_bar:
                for file in file_list:
                    zip_ref.extract(file, file_directory)
                    progress_bar.update(1)
            print(f"Complete to extract file: {file_name} to {file_path}")
    else:
        if not file_path.endswith(".tar.gz"):
            raise ValueError(f"File {file_path} is not .tar.gz on Linux.")
        with tarfile.open(file_path, 'r:gz') as tar_ref:
            file_list = tar_ref.getnames()
            print(f"Begin to extract file: {file_name} to {file_path}")
            with tqdm_wrap(total=len(file_list), unit='File', miniters=1) as progress_bar:
                for file in file_list:
                    tar_ref.extract(file, file_directory)
                    progress_bar.update(1)
            print(f"Complete to extract file: {file_name} to {file_path}")


def download_sdk(platform, target_type, download_dir, info, sdk_dir):
    mkdir_if_not_exists(download_dir)
    url = info['sdk'][target_type]
    download(url, download_dir)
    _, file_name = os.path.split(url)
    extract(platform, download_dir, file_name)
    shutil.copytree(os.path.join(download_dir, "cangjie"), sdk_dir)
    shutil.rmtree(download_dir, ignore_errors=True)


def move_result(args):
    if not os.path.exists(OUTPUT_DIR):
        os.mkdir(OUTPUT_DIR)

    output_dir=os.path.join(OUTPUT_DIR, args.target_type)
    if os.path.exists(output_dir):
        shutil.rmtree(output_dir)
    os.mkdir(output_dir)

    generate_version_list(os.path.join(output_dir, "version_list"), version_list)
    generate_version_list(os.path.join(output_dir, "version_list_deveco"), version_list_deveco)
    if args.platform.is_windows():
        cjm_name = "cjm.exe"
    else:
        cjm_name = "cjm"


    shutil.copy(os.path.join(CJM_BUILD_DIR, f"src/{cjm_name}"), os.path.join(output_dir, cjm_name))
    if args.platform.is_windows():
        shutil.copy(os.path.join(CJM_BUILD_DIR + "/platform", "libplatform.dll"), os.path.join(output_dir, "libplatform.dll"))
    elif args.platform.is_linux():
        shutil.copy(os.path.join(CJM_BUILD_DIR + "/platform", "libplatform.so"), os.path.join(output_dir, "libplatform.so"))
    else: # Darwin
        shutil.copy(os.path.join(CJM_BUILD_DIR + "/platform", "libplatform.dylib"), os.path.join(output_dir, "libplatform.dylib"))

    target_versions_dir = os.path.join(output_dir, "versions")
    mkdir_if_not_exists(target_versions_dir)
    for version, info in version_list.items():
        target_version_dir = os.path.join(target_versions_dir, version)
        mkdir_if_not_exists(target_version_dir)
        source_version_dir = os.path.join(VERSIONS_DIR, version)
        shutil.copytree(os.path.join(source_version_dir, "rules"),os.path.join(target_version_dir, "rules"))
        if args.platform.is_windows():
            target_name = f"libcjm_{version}.dll"
            shutil.copy(os.path.join(CJM_BUILD_DIR + "/versions/" + version + "/src", target_name), os.path.join(target_version_dir, "libcjm.dll"))
        elif args.platform.is_linux():
            target_name = f"libcjm_{version}.so"
            shutil.copy(os.path.join(CJM_BUILD_DIR + "/versions/" + version + "/src", target_name), os.path.join(target_version_dir, "libcjm.so"))
        else: # Darwin
            target_name = f"libcjm_{version}.dylib"
            shutil.copy(os.path.join(CJM_BUILD_DIR + "/versions/" + version + "/src", target_name), os.path.join(target_version_dir, "libcjm.dylib"))

    target_doc_dir = os.path.join(output_dir, "docs")
    mkdir_if_not_exists(target_doc_dir)
    shutil.copy(os.path.join(HOME_DIR, "README.md"), os.path.join(target_doc_dir, "README.md"))
    shutil.copy(os.path.join(HOME_DIR, "Update.md"), os.path.join(target_doc_dir, "Update.md"))


def prepare_dependency(args):
    LOG.info('begin prepare dependency...\n')
    for version, info in version_list.items():
        version_dir = os.path.join(VERSIONS_DIR, version)
        lib_dir = os.path.join(version_dir, "lib")
        cangjie_dir = os.path.join(lib_dir, "cangjie")
        if not os.path.exists(cangjie_dir):
            mkdir_if_not_exists(lib_dir)
            output = subprocess.Popen(
                ["git", "clone", "-b", info['tag'], CANGJIE_REPO, cangjie_dir],
                cwd = lib_dir,
                stdout = PIPE,
            )
            log_output(output)
            LOG.info("cangjie " + version + " cloned")
        sdk_dir=os.path.join(os.path.join(lib_dir, "sdks"), args.target_type)
        if not os.path.exists(sdk_dir):
            download_sdk(args.platform, args.target_type, os.path.join(version_dir, "download"), info, sdk_dir)
    LOG.info('end prepare dependency...\n')


def valid_target_platform(target: Platform):
    current = Platform.current()
    if current == target:
        return True
    if current == Platform.LINUX and target == Platform.WINDOWS:
        return True
    return False


def build(args):
    args.target_type = args.platform.value + '-' + args.architecture.value
    LOG.info('begin build...\n')
    prepare_dependency(args)
    LOG.info('begin build cjm\n')
    build_target(args, HOME_DIR, CJM_BUILD_DIR)
    LOG.info('end build cjm\n')
    move_result(args)
    LOG.info('end build\n')


def build_target(args, home_dir, build_dir):
    if not os.path.exists(build_dir):
        os.makedirs(build_dir)
        output = subprocess.Popen(['cmake', home_dir, '-G', "Ninja"] +
                                  generate_cmake_defs(args), cwd=build_dir, stdout=PIPE)
        log_output(output)
    output = subprocess.Popen(['ninja'], cwd=build_dir, stdout=PIPE)
    log_output(output)


def clean(args):
    """clean build outputs and logs"""
    LOG.info("begin clean...\n")
    output_dirs = [
        "build",
    ]
    if args.all:
        output_dirs.append("lib/" + args.version + "/cangjie")
        output_dirs.append("lib/" + args.version + "/cjfmt")
    elif args.cangjie:
        output_dirs.append("lib/" + args.version + "/cangjie")
    for file_path in output_dirs:
        abs_file_path = os.path.join(HOME_DIR, file_path)
        LOG.info(abs_file_path)
        if os.path.isdir(abs_file_path):
            shutil.rmtree(abs_file_path, ignore_errors=True)
        if os.path.isfile(abs_file_path):
            os.remove(abs_file_path)
    LOG.info("end clean\n")


class BuildType(Enum):
    """CMAKE_BUILD_TYPE options"""
    debug = 'Debug'
    release = 'Release'
    relwithdebinfo = 'RelWithDebInfo'

    def __str__(self):
        return self.name

    def __repr(self):
        return str(self)

    @staticmethod
    def argparse(s):
        try:
            return BuildType[s]
        except KeyError:
            return s

def run_test(args):
    """Run test cases"""
    LOG.info('begin test...\n')

    current_build_type=get_current_build_type()
    output_dir=os.path.join(OUTPUT_DIR, current_build_type)
    cjm_path = os.path.join(output_dir, 'cjm')
    if not os.path.exists(cjm_path):
        LOG.error('cjm not found in output directory!')
        sys.exit(1)

    # 默认处理 test 目录
    if not args.directory and not args.file:
        args.directory = 'test'

    env = os.environ.copy()
    env["CANGJIE_HOME"] = VERSIONS_DIR + "/0.53.18/lib/sdks/" + current_build_type

    if args.directory:
        test_dir = args.directory
        if not os.path.isdir(test_dir):
            LOG.error(f'Directory {test_dir} does not exist!')
            sys.exit(1)

        # 遍历目录中的所有.cj文件
        for root, _, files in os.walk(test_dir):
            for file in files:
                if file.endswith('.cj'):
                    file_path = os.path.join(root, file)
                    # 检查文件名是否为纯数字
                    if re.match(r'^\d+\.cj$', file):
                        output_path = os.path.join(root, f'{os.path.splitext(file)[0]}_output.cj')
                        LOG.info(f'Running test on {file_path}')
                        try:
                            subprocess.run([
                                cjm_path,
                                file_path,
                                '-o',
                                output_path
                            ], env = env, check = True)
                            LOG.info(f'Successfully generated {output_path}')
                        except subprocess.CalledProcessError as e:
                            LOG.error(f'Error processing {file_path}: {str(e)}')
                            sys.exit(1)
    elif args.file:
        test_file = os.path.abspath(args.file)
        if not os.path.isfile(test_file):
            LOG.error(f'File {test_file} does not exist!')
            sys.exit(1)

        output_path = os.path.join(os.path.dirname(test_file), f'{os.path.splitext(test_file)[0]}_output.cj')
        LOG.info(f'Running test on {test_file}')
        try:
            subprocess.run([
                cjm_path,
                test_file,
                '-o',
                output_path
            ], env = env, check = True)
            LOG.info(f'Successfully generated {output_path}')
        except subprocess.CalledProcessError as e:
            LOG.error(f'Error processing {test_file}: {str(e)}')
            sys.exit(1)

    LOG.info('end test\n')


def add_command_build(subparsers):
    parser_build = subparsers.add_parser('build', help='build migrator')
    parser_build.add_argument('-p', '--platform',
                              type=Platform.argparse,
                              dest='platform',
                              default=Platform.current(),
                              choices=list(Platform),
                              help='select target platform')
    parser_build.add_argument('-a', '--architecture',
                              type=Architecture.argparse,
                              dest='architecture',
                              default=Architecture.current(),
                              choices=list(Architecture),
                              help='select target architecture')
    parser_build.set_defaults(func=build)


def add_command_clean(subparsers):
    parser_clean = subparsers.add_parser("clean", help="clean build")
    parser_clean.add_argument('--cangjie', action='store_true', help='clean cangjie')
    parser_clean.add_argument('--cjfmt', action='store_true', help='clean cjfmt')
    parser_clean.add_argument('--all', action='store_true', help='clean all')
    parser_clean.add_argument(
        "--version", dest="version", type=str, default="", help="choose version for cangjie and cjfmt")
    parser_clean.set_defaults(func=clean)


def add_command_test(subparsers):
    parser_test = subparsers.add_parser('test', help='run tests')
    test_group = parser_test.add_mutually_exclusive_group()
    test_group.add_argument('-d', dest='directory', help='test directory containing .cj files')
    test_group.add_argument('-f', dest='file', help='test single .cj file')
    parser_test.set_defaults(func=run_test)


def main():
    parser = argparse.ArgumentParser(description='build migrator project')
    subparsers = parser.add_subparsers(help='sub command help')

    add_command_build(subparsers)
    add_command_clean(subparsers)
    add_command_test(subparsers)

    args = parser.parse_args()
    if not hasattr(args, 'func'):
        args = parser.parse_args(['build'] + sys.argv[1:])

    args.func(args)


if __name__ == '__main__':
    LOG = init_log('root')
    os.environ['LANG'] = "C.UTF-8"
    main()
